package com.netintech.project.system.videoPacketLossRate.service;

import com.google.common.collect.Lists;
import com.netintech.common.constant.Constants;
import com.netintech.common.exception.lossRateCheck.FileNotFindException;
import com.netintech.common.exception.lossRateCheck.isCheckingException;
import com.netintech.common.utils.CollectionUtils;
import com.netintech.common.utils.poi.ExcelUtil;
import com.netintech.core.aspectj.lang.annotation.DataSource;
import com.netintech.core.aspectj.lang.enums.DataSourceType;
import com.netintech.core.manager.AsyncManager;
import com.netintech.core.manager.factory.AsyncFactory;
import com.netintech.project.system.ffmpeg.FfmpegUtil;
import com.netintech.project.system.ffmpeg.bo.FfmpegProperties;
import com.netintech.project.system.videoPacketLossRate.bo.ExcelFileBo;
import com.netintech.project.system.videoPacketLossRate.bo.SlideWindowBo;
import com.netintech.project.system.videoPacketLossRate.domain.Camera;
import com.netintech.project.system.videoPacketLossRate.mapper.CameraMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import java.io.*;
import java.text.NumberFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;


@Slf4j
@Service
@EnableConfigurationProperties(FfmpegProperties.class)
public class LossRateServiceImpl implements LossRateService {


    @Autowired
    private FfmpegProperties ffmpegProperties;
    @Autowired
    private CameraMapper cameraMapper;

    private static NumberFormat numberFormat;

    /**
     * 从excel获取的数据检测是否进行中
     */
    private volatile boolean isChecking = false;

    /**
     * 从数据库获取的数据检测是否进行中
     */
    private volatile boolean dataFromDbIsChecking = false;

    public AtomicInteger isCheckingThreadNum = new AtomicInteger(0);

    public AtomicInteger dataFromDbIsCheckingThreadNum = new AtomicInteger(0);


    static {
        numberFormat = NumberFormat.getInstance();
        numberFormat.setGroupingUsed(false);
        // 设置精确到小数点后2位
        numberFormat.setMaximumFractionDigits(2);
    }

    /**
     * 初始化excel检测的相关静态变量
     */
    @Override
    public void excelInit(Exception e) {
//        抛出正在检测异常时，不用初始化
        if (!(e instanceof isCheckingException)) {
            isChecking = false;
            isCheckingThreadNum.set(0);
        }
    }

    /**
     * 初始化db检测的相关静态变量
     */
    @Override
    public void dbInit(Exception e) {
//        抛出正在检测异常时，不用初始化
        if (!(e instanceof isCheckingException)) {
            dataFromDbIsChecking = false;
            dataFromDbIsCheckingThreadNum.set(0);
        }
    }


    @Override
    public String execute(Integer seconds, String transportType, String rtspUrl) throws IOException, InterruptedException {
        Map<String, String> map = genMsgByRtspUrl(rtspUrl);
        String seqNumFilePath = map.get("seqNumFilePath");
        String outputVideoPath = map.get("outputVideoPath");
//        log.info("seqNumFilePath-->" + seqNumFilePath);
//        log.info("outputVideoPath-->" + outputVideoPath);
        UUID uuid = UUID.randomUUID();
//        1. 删除文件
//        deleteQuietly不会抛出异常
        FileUtils.deleteQuietly(new File(seqNumFilePath));
        FileUtils.deleteQuietly(new File(outputVideoPath));


//        2.调用ffmpeg.exe 执行指定时间，生成seqNum文件
        FfmpegUtil.execute(ffmpegProperties.getExePath(), transportType,
                rtspUrl, outputVideoPath, uuid, seconds, ffmpegProperties.getFileDirPath());

//3. 读取seqNum文件，计算丢包率
        return readAndCompute(seqNumFilePath, outputVideoPath);
    }

    @Override
    public void executeFromExcel(Integer seconds, String transportType) throws Exception {
//        控制只有一个正在检测的线程
        if (isChecking) {
            throw new isCheckingException("正在检测中，请稍后再试");
        }
        isChecking = true;
//        获取excel文件流
//        File file = new File(ffmpegProperties.getExcelFilePath());
        Resource resource = new ClassPathResource(ffmpegProperties.getExcelInPutFilePath());
        InputStream excelFilePathIs = resource.getInputStream();
//        1.读取excel中设备信息 到list中
        ExcelUtil<ExcelFileBo> excelUtil = new ExcelUtil<>(ExcelFileBo.class);
//        List<ExcelFileBo> excelFileBos = excelUtil.importExcel(new FileInputStream(file));
        List<ExcelFileBo> excelFileBos = excelUtil.importExcel(excelFilePathIs);
        Integer reTryThreadIntervalMilliSeconds = ffmpegProperties.getReTryThreadIntervalMilliSeconds();
        for (ExcelFileBo excelFileBo : excelFileBos) {
            if (isCheckingThreadNum.get() == ffmpegProperties.getThreadNum() || isCheckingThreadNum.get() > ffmpegProperties.getThreadNum()) {
                log.info("超过同时运行线程上限，等待中...");
                Thread.sleep(seconds + reTryThreadIntervalMilliSeconds);
            }
            String rtspUrl = excelFileBo.getRtspUrl();
            if (StringUtils.isBlank(rtspUrl)) {
                log.info("rtspUrl为空-->" + excelFileBo.getDeviceName());
                break;
            }
            isCheckingThreadNum.incrementAndGet();
//        多线程调用ffmpeg进行检测
            AsyncManager.me().execute(AsyncFactory.checkLossRateFromExcel(excelFileBo, this, seconds, transportType, rtspUrl));
        }
//        等待全部执行完
        int max = getCounterMax(seconds, reTryThreadIntervalMilliSeconds);
        while (isCheckingThreadNum.get() != 0) {
            log.info("正在等待未执行完的线程数-->" + isCheckingThreadNum.get());
            Thread.sleep(reTryThreadIntervalMilliSeconds);
            max--;
            if (terminateCheckCounter(max)) {
                break;
            }
        }
        log.info("全部检测完毕！");
        excelUtil.exportExcel2(excelFileBos, "Sheet1", genExcelOutPutFilePath(ffmpegProperties.getExcelOutPutFilePath()));
        log.info("导出结果到excel完毕！");
    }


    /**
     * 终止检测线程计数，防止有进程不结束，程序不结束
     *
     * @param max
     * @return 是否退出
     */
    private boolean terminateCheckCounter(int max) {
        if (max > 0) {
            log.info("剩余等待测试次数" + max);
            return false;
        }
        return true;
    }

    /**
     * 终止计数器上限值
     *
     * @param checkIntervalMilliSeconds
     * @param reTryThreadIntervalMilliSeconds
     * @return
     */
    private int getCounterMax(double checkIntervalMilliSeconds, Integer reTryThreadIntervalMilliSeconds) {
        //        向上取整
        Double ceil = Math.ceil(checkIntervalMilliSeconds / reTryThreadIntervalMilliSeconds);
        int i = ceil.intValue();
        int max = 5 * ffmpegProperties.getThreadNum() * i;
        log.info("counterMax-->" + max);
        return max;
    }


    /**
     * excel输出文件加上时间区分
     *
     * @param outPutFilePath
     */
    private String genExcelOutPutFilePath(String outPutFilePath) {
        if (StringUtils.isBlank(outPutFilePath)) {
            throw new FileNotFindException("未配置excel输出文件路径");
        }
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy_MM_dd_HH_mm_ss");
        String time = dateTimeFormatter.format(LocalDateTime.now());
        return outPutFilePath.concat(time).concat(".xlsx");
    }


    @Override
    @DataSource(value = DataSourceType.SLAVE)
    public void executeFromDb(Integer seconds, String transportType) throws InterruptedException {
        //        控制只有一个正在检测的线程
        if (dataFromDbIsChecking) {
            throw new isCheckingException("正在检测中，请稍后再试");
        }
        dataFromDbIsChecking = true;
//        1.读取数据库中设备信息 到list中
        ExcelUtil<ExcelFileBo> excelUtil = new ExcelUtil<>(ExcelFileBo.class);

        List<Camera> cameras = cameraMapper.selAll();

//                genCamPlanUrl
        List<ExcelFileBo> excelFileBos = cameras.stream().map(a -> ExcelFileBo.builder()
                .deviceName(a.getCName())
                .rtspUrl(genCamPlanUrl(a))
                .build()
        ).collect(Collectors.toList());


        Integer reTryThreadIntervalMilliSeconds = ffmpegProperties.getReTryThreadIntervalMilliSeconds();
        for (ExcelFileBo excelFileBo : excelFileBos) {
            if (dataFromDbIsCheckingThreadNum.get() == ffmpegProperties.getThreadNum() || dataFromDbIsCheckingThreadNum.get() > ffmpegProperties.getThreadNum()) {
                log.info("超过同时运行线程上限，等待中...");
                Thread.sleep(seconds + reTryThreadIntervalMilliSeconds);
            }
            String rtspUrl = excelFileBo.getRtspUrl();
            if (StringUtils.isBlank(rtspUrl)) {
                log.info("rtspUrl为空-->" + excelFileBo.getDeviceName());
                break;
            }
            dataFromDbIsCheckingThreadNum.incrementAndGet();
//        多线程调用ffmpeg进行检测
            AsyncManager.me().execute(AsyncFactory.checkLossRateFromDb(excelFileBo, this, seconds, transportType, rtspUrl));
        }

//        等待全部执行完
        int max = getCounterMax(seconds, reTryThreadIntervalMilliSeconds);
        while (dataFromDbIsCheckingThreadNum.get() != 0) {
            log.info("正在等待未执行完的线程数-->" + dataFromDbIsCheckingThreadNum.get());
            Thread.sleep(reTryThreadIntervalMilliSeconds);
            max--;
            if (terminateCheckCounter(max)) {
                break;
            }
        }
        log.info("全部检测完毕！");
        excelUtil.exportExcel2(excelFileBos, "Sheet1", genExcelOutPutFilePath(ffmpegProperties.getExcelOutPutFilePath()));
        log.info("导出结果到excel完毕！");
    }


    /**
     * 生成相机的rtsp url
     * 通过NVR设备的相机【目前判断方式不确定，默认全通过nvr】，url中的ip等信息取nvr 的；
     * 否则url中ip等信息取设备的，且通道号设置为1
     *
     * @param camera
     * @return
     */
    private String genCamPlanUrl(Camera camera) {
        final int CAMERA_JOIN_TYPE_HIK = 0;
        final int CAMERA_JOIN_TYPE_DAHUA = 1;
        Integer chan;
        String ip;
        String username;
        String password;


        ip = camera.getNvrIp();
        chan = camera.getNvrChan();
        username = camera.getNvrUsername();
        password = camera.getNvrPassword();


//        大华规则: rtsp://username:password@ip:port/cam/realmonitor?channel=通道号&subtype=0
//        海康规则:rtsp://用户名:密码@IP地址/ch通道号/main/av_stream
        StringBuilder result = new StringBuilder("rtsp://");
        result.append(username).append(":").append(password).append("@").append(ip);
        Integer joinType = camera.getJoinType();
        if (joinType == null) {
            return null;
        }
        switch (joinType) {
            case CAMERA_JOIN_TYPE_HIK:
                result.append("/ch").append(chan).append("/main/av_stream");
                break;
            case CAMERA_JOIN_TYPE_DAHUA:
                result.append("/cam/realmonitor?channel=")
                        .append(chan).append("&subtype=0");
                break;
            default:
                return null;
        }
        return result.toString();
    }


    /**
     * 根据rtsp地址，得出seqNum文件名称、视频文件名称
     *
     * @param rtspUrl
     * @return
     */
    private Map<String, String> genMsgByRtspUrl(String rtspUrl) {
        Map<String, String> map = new HashMap<>();
        String substring1 = rtspUrl.substring(rtspUrl.indexOf("@") + 1);
        //将"/"转成"_"
        String replace = substring1.replace("/", "_");
        String seqNumFilePath = ffmpegProperties.getFileDirPath() + replace;
        String outputVideoPath = ffmpegProperties.getFileDirPath() + replace + ".mp4";
        map.put("seqNumFilePath", seqNumFilePath);
        map.put("outputVideoPath", outputVideoPath);
        return map;
    }

    /**
     * 读取seqNum文件，计算丢包率
     *
     * @return
     * @throws IOException
     */
    private String readAndCompute(String seqNumFilePath, String outputVideoPath) throws IOException {
        long l1 = System.currentTimeMillis();
//        按行读取
        List<String> strList = FileUtils.readLines(new File(seqNumFilePath));
        List<Integer> integerList = strList.stream().map(Integer::parseInt).collect(Collectors.toList());
        log.info("读取文件" + seqNumFilePath + "--->中seqNum总个数" + integerList.size());
        String lossRate = computePacketLossRate(seqNumFilePath, integerList, outputVideoPath);
        long l2 = System.currentTimeMillis();
        log.info("读取文件" + seqNumFilePath + "--->、计算丢包率，耗时毫秒数--->" + (l2 - l1));
        return lossRate;
    }

    /**
     * 计算丢包率，当丢包数大于0时保留视频文件（否则，删除）
     *
     * @param seqNumFilePath
     * @param sequenceNumList
     * @param outputVideoPath
     * @return 丢包率
     */
    private String computePacketLossRate(String seqNumFilePath, List<Integer> sequenceNumList, String outputVideoPath) {
        if (CollectionUtils.isEmpty(sequenceNumList)) {
            return seqNumFilePath + "--->索引数集合为空";
        }
//        -1，因为要尾加一个元素
        final int partitionSize = Constants.SLIDE_WINDOW_LENGTH - 1;
//        分组数（0、1、2...511）临界值
        final int groupCriticalNum = Constants.RTP_SEQ_NUM_LOOP_COUNT / Constants.SLIDE_WINDOW_LENGTH - 1;
//        总丢包数
        int packetLossCount = 0;
//        不丢包情况下的总数
        int totalCount = 0;

//        分组排序后的序列号（要先在组内按照初始值排序）
        List<List<Integer>> partitionNew = partitionAndSort(sequenceNumList, partitionSize);
//        将分组的序列号集合转成分组的滑动窗口对象
        List<List<SlideWindowBo>> slideWindowBos = partitionNew.stream().map(this::transSlideWindowBo).collect(Collectors.toList());

//        对分组的滑动窗口对象，分析丢包率
        for (List<SlideWindowBo> slideWindowBo : slideWindowBos) {
            int slideWindowBoSize = slideWindowBo.size();
//            集合内元素小于等于1，无法比较
            if (slideWindowBoSize > 1) {
                for (int i = 0; i < slideWindowBoSize - 1; i++) {
//                    比较相邻元素的映射值
                    SlideWindowBo slideWindowBo1 = slideWindowBo.get(i);
                    SlideWindowBo slideWindowBo2 = slideWindowBo.get(i + 1);
                    Integer indexValue1 = slideWindowBo1.getIndexValue();
                    Integer indexValue2 = slideWindowBo2.getIndexValue();
                    Integer count1 = slideWindowBo1.getCount();
                    Integer count2 = slideWindowBo2.getCount();
                    int indexValueInterval = indexValue2 - indexValue1;
                    int countInterval = count2 - count1;
//                    同组，（...、65535、0、...也认为是同组）
                    if (countInterval == 0 || countInterval == -groupCriticalNum) {
//                             未丢包
                        if (indexValueInterval == -Constants.SLIDE_WINDOW_LENGTH + 1 || indexValueInterval == 1) {
                            totalCount++;
                            continue;
                        }

                        if (indexValueInterval > 1) {
                            log.info(seqNumFilePath + "--->发生丢包" + slideWindowBo1.getOriginalValue() + "、" + slideWindowBo2.getOriginalValue());
                            packetLossCount += indexValueInterval - 1;
                            totalCount += indexValueInterval;
                        }
                        if (indexValueInterval < 0) {
                            log.info(seqNumFilePath + "--->发生丢包" + slideWindowBo1.getOriginalValue() + "、" + slideWindowBo2.getOriginalValue());
                            packetLossCount += Constants.SLIDE_WINDOW_LENGTH + indexValueInterval - 1;
                            totalCount += Constants.SLIDE_WINDOW_LENGTH + indexValueInterval;
                        }
                        continue;
                    }

//                        丢了整组数据
                    if (countInterval == 1) {
                        if (indexValueInterval == -Constants.SLIDE_WINDOW_LENGTH + 1) {
//                            未丢包
                            totalCount++;
                            continue;
                        }
                        if (indexValueInterval < 0) {
                            log.info(seqNumFilePath + "--->发生丢包" + slideWindowBo1.getOriginalValue() + "、" + slideWindowBo2.getOriginalValue());
                            packetLossCount += Constants.SLIDE_WINDOW_LENGTH + indexValueInterval - 1;
                            totalCount += Constants.SLIDE_WINDOW_LENGTH + indexValueInterval;
                        } else {
                            log.info(seqNumFilePath + "--->发生丢包" + slideWindowBo1.getOriginalValue() + "、" + slideWindowBo2.getOriginalValue());
                            packetLossCount += countInterval * Constants.SLIDE_WINDOW_LENGTH + indexValueInterval - 1;
                            totalCount += countInterval * Constants.SLIDE_WINDOW_LENGTH + indexValueInterval;
                        }
                        continue;
                    }

                    if (countInterval > 1) {
                        log.info(seqNumFilePath + "--->发生丢包" + slideWindowBo1.getOriginalValue() + "、" + slideWindowBo2.getOriginalValue());
                        packetLossCount += countInterval * Constants.SLIDE_WINDOW_LENGTH + indexValueInterval - 1;
                        totalCount += countInterval * Constants.SLIDE_WINDOW_LENGTH + indexValueInterval;
                        continue;
                    }
//                    countInterval<0且不等于-511的情况
                    if (indexValueInterval < 0) {
                        log.info(seqNumFilePath + "--->发生丢包" + slideWindowBo1.getOriginalValue() + "、" + slideWindowBo2.getOriginalValue());
                        packetLossCount += (countInterval + groupCriticalNum) * Constants.SLIDE_WINDOW_LENGTH +
                                (Constants.SLIDE_WINDOW_LENGTH + indexValueInterval) - 1;
                        totalCount += (countInterval + groupCriticalNum) * Constants.SLIDE_WINDOW_LENGTH +
                                (Constants.SLIDE_WINDOW_LENGTH + indexValueInterval);

                    } else {
                        log.info(seqNumFilePath + "--->发生丢包" + slideWindowBo1.getOriginalValue() + "、" + slideWindowBo2.getOriginalValue());
                        packetLossCount += (countInterval + groupCriticalNum) * Constants.SLIDE_WINDOW_LENGTH + indexValueInterval - 1;
                        totalCount += (countInterval + groupCriticalNum) * Constants.SLIDE_WINDOW_LENGTH + indexValueInterval;
                    }

                }
            }
        }
        totalCount++;
//        当丢包数等于0时删除视频文件
        log.info(seqNumFilePath + "--->丢包数为" + packetLossCount);
        if (packetLossCount == 0) {
            FileUtils.deleteQuietly(new File(outputVideoPath));
            log.info("成功删除视频文件" + outputVideoPath);
        }

        String packetLossRate = numberFormat.format((double) packetLossCount / totalCount * 100) + "%";
        log.info(seqNumFilePath + "--->丢包率为" + packetLossRate);
        return packetLossRate;
    }

    /**
     * 分组并组内排序
     *
     * @param sequenceNumList
     * @param partitionSize
     * @return
     */
    private List<List<Integer>> partitionAndSort(List<Integer> sequenceNumList, int partitionSize) {
        List<List<Integer>> partitionNew = new ArrayList<>();
//        list分组
        List<List<Integer>> partition = Lists.partition(sequenceNumList, partitionSize);
//        如果分组后有多组集合（>1），将下一组的首个元素加到该组末尾，为了防止分组间临界点存在丢包情况
        if (partition.size() > 1) {
            for (int i = 0; i < partition.size() - 1; i++) {
                List<Integer> integers = partition.get(i);
                List<Integer> next = partition.get(i + 1);
                Integer integer = next.get(0);
                List<Integer> integers1 = new ArrayList<>(integers);
                integers1.add(integer);
                integers1 = sortSeqNum(integers1);
                partitionNew.add(integers1);
            }
//            加上最后一个
            List<Integer> last = partition.get(partition.size() - 1);
            last = sortSeqNum(last);
            partitionNew.add(last);
        } else {
            partitionNew.add(sortSeqNum(partition.get(0)));
        }
        return partitionNew;
    }

    /**
     * 将索引数字转换成滑动窗口对象
     *
     * @param sequenceNumList
     * @return
     */
    private List<SlideWindowBo> transSlideWindowBo(List<Integer> sequenceNumList) {
        return sequenceNumList.stream().map(a -> SlideWindowBo.builder()
                .count(a / Constants.SLIDE_WINDOW_LENGTH)
                .indexValue(a % Constants.SLIDE_WINDOW_LENGTH)
                .originalValue(a)
                .build()).collect(Collectors.toList());
    }

    //
    public static void main(String[] args) {

        LossRateServiceImpl lossRateService = new LossRateServiceImpl();
        try {
            String compute = lossRateService.readAndCompute(
                    "C:\\Users\\Administrator\\Desktop\\421-422\\170.18.0.196_cam_realmonitor_channel=21&subtype=0",
//                    "C:\\Users\\Administrator\\Desktop\\421-422\\170.25.0.20_cam_realmonitor_channel=11&subtype=0",
//                    "C:\\Users\\Administrator\\Desktop\\421-422\\170.7.2.239_ch40_main_av_stream",
//                    "C:\\Users\\Administrator\\Desktop\\421-422\\170.4.1.89_ch41_main_av_stream",
//                    "C:\\Users\\Administrator\\Desktop\\421-422\\170.26.1.2_ch47_main_av_stream",
//                    "C:\\Users\\Administrator\\Desktop\\421-422\\170.26.0.242_ch45_main_av_stream",
                    "C:\\Users\\Administrator\\Desktop\\aaa.mp4");
            System.out.println("丢包率" + compute);
        } catch (IOException e) {
            e.printStackTrace();
        }


    }

    /**
     * 对数字集合进行自定义从小到大排序
     *
     * @param sequenceNumList
     * @return
     */
    private List<Integer> sortSeqNum(List<Integer> sequenceNumList) {
        List<Integer> result = new ArrayList<>();
        Integer max = Collections.max(sequenceNumList);
        Integer min = Collections.min(sequenceNumList);
        int indexOfMax = sequenceNumList.indexOf(max);
        int indexOfMin = sequenceNumList.indexOf(min);

//        最大值在最小值前面
        if (indexOfMax < indexOfMin &&
                (max - min) > Constants.RTP_SEQ_NUM_LOOP_COUNT * Constants.SEQ_NUM_GROUP_MAX_MIN_INTERVAL_PERCENT) {

            List<Integer> maxGroup = sequenceNumList.subList(0, indexOfMin);
            List<Integer> minGroup = sequenceNumList.subList(indexOfMin, sequenceNumList.size());

            maxGroup.sort(Integer::compareTo);
            minGroup.sort(Integer::compareTo);

            result.addAll(maxGroup);
            result.addAll(minGroup);

        } else {
            sequenceNumList.sort(Integer::compareTo);
            result = sequenceNumList;
        }
        return result;
    }


}
